MATRIX_NORMAL

Overview

The MATRIX_NORMAL function computes the probability density function (PDF), log-PDF, or draws random samples from a matrix normal distribution (also known as the matrix Gaussian distribution). This distribution is a generalization of the multivariate normal distribution to matrix-valued random variables, making it useful for modeling data that naturally takes the form of matrices, such as repeated multivariate measurements or spatiotemporal data.

The function uses SciPy’s scipy.stats.matrix_normal implementation. A random matrix X of dimensions n \times p follows the matrix normal distribution \mathcal{MN}_{n,p}(M, U, V) with mean matrix M, among-row covariance matrix U (of size n \times n), and among-column covariance matrix V (of size p \times p).

The probability density function is defined as:

p(X \mid M, U, V) = \frac{\exp\left(-\frac{1}{2}\text{tr}\left[V^{-1}(X - M)^T U^{-1}(X - M)\right]\right)}{(2\pi)^{np/2} |V|^{n/2} |U|^{p/2}}

where \text{tr} denotes the matrix trace. The matrix normal distribution is equivalent to the multivariate normal distribution with the relationship \text{vec}(X) \sim \mathcal{N}_{np}(\text{vec}(M), V \otimes U), where \otimes denotes the Kronecker product and \text{vec} is the vectorization operator.

The separate row and column covariance matrices capture dependencies in two dimensions: U models correlations among the rows of X, while V models correlations among the columns. This separable covariance structure reduces the number of parameters compared to a full multivariate normal covariance matrix from (np)^2 to n^2 + p^2.

For further theoretical background, see the Matrix normal distribution article on Wikipedia. The SciPy implementation documentation is available in the SciPy reference.

This example function is provided as-is without any representation of accuracy.

Excel Usage

=MATRIX_NORMAL(x, mean, rowcov, colcov, mn_method, size)
  • x (list[list], optional, default: null): Matrix at which to evaluate the function or template for sample shape.
  • mean (list[list], optional, default: null): Mean matrix of the distribution. Must match shape of x. Default is zero matrix.
  • rowcov (list[list], optional, default: null): Among-row covariance matrix. Must be square with size equal to number of rows in x. Default is identity matrix.
  • colcov (list[list], optional, default: null): Among-column covariance matrix. Must be square with size equal to number of columns in x. Default is identity matrix.
  • mn_method (str, optional, default: “pdf”): Method to compute. Valid options are pdf, logpdf, or rvs.
  • size (int, optional, default: 1): Number of samples to draw if method is rvs.

Returns (list[list]): 2D list of results, or error message string.

Examples

Example 1: PDF with identity covariances

Inputs:

x mean rowcov colcov mn_method
1 2 0 0 1 0 1 0 pdf
3 4 0 0 0 1 0 1

Excel formula:

=MATRIX_NORMAL({1,2;3,4}, {0,0;0,0}, {1,0;0,1}, {1,0;0,1}, "pdf")

Expected output:

Result
7.749e-9

Example 2: LogPDF with identity covariances

Inputs:

x mean rowcov colcov mn_method
1 2 0 0 1 0 1 0 logpdf
3 4 0 0 0 1 0 1

Excel formula:

=MATRIX_NORMAL({1,2;3,4}, {0,0;0,0}, {1,0;0,1}, {1,0;0,1}, "logpdf")

Expected output:

Result
-18.68

Example 3: PDF with default mean (zero matrix)

Inputs:

x rowcov colcov mn_method
0 0 1 0 1 0 pdf
0 0 0 1 0 1

Excel formula:

=MATRIX_NORMAL({0,0;0,0}, {1,0;0,1}, {1,0;0,1}, "pdf")

Expected output:

Result
0.02533

Example 4: LogPDF with scaled covariance matrices

Inputs:

x mean rowcov colcov mn_method
0.5 0.5 0 0 2 0 2 0 logpdf
0.5 0.5 0 0 0 2 0 2

Excel formula:

=MATRIX_NORMAL({0.5,0.5;0.5,0.5}, {0,0;0,0}, {2,0;0,2}, {2,0;0,2}, "logpdf")

Expected output:

Result
-6.573

Python Code

from scipy.stats import matrix_normal as scipy_matrix_normal

def matrix_normal(x=None, mean=None, rowcov=None, colcov=None, mn_method='pdf', size=1):
    """
    Computes the PDF, log-PDF, or draws random samples from a matrix normal distribution.

    See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.matrix_normal.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        x (list[list], optional): Matrix at which to evaluate the function or template for sample shape. Default is None.
        mean (list[list], optional): Mean matrix of the distribution. Must match shape of x. Default is zero matrix. Default is None.
        rowcov (list[list], optional): Among-row covariance matrix. Must be square with size equal to number of rows in x. Default is identity matrix. Default is None.
        colcov (list[list], optional): Among-column covariance matrix. Must be square with size equal to number of columns in x. Default is identity matrix. Default is None.
        mn_method (str, optional): Method to compute. Valid options are pdf, logpdf, or rvs. Valid options: PDF, LogPDF, RVS. Default is 'pdf'.
        size (int, optional): Number of samples to draw if method is rvs. Default is 1.

    Returns:
        list[list]: 2D list of results, or error message string.
    """
    def to2d(val):
        return [[val]] if not isinstance(val, list) else val

    x = to2d(x)
    mean = to2d(mean) if mean is not None else None
    rowcov = to2d(rowcov) if rowcov is not None else None
    colcov = to2d(colcov) if colcov is not None else None

    # Validate x
    if not isinstance(x, list) or len(x) < 1 or not all(isinstance(row, list) and len(row) > 0 for row in x):
        return "Invalid input: x must be a 2D list with at least one row."
    try:
        x_mat = [[float(val) for val in row] for row in x]
    except Exception:
        return "Invalid input: x must contain numeric values."
    n_rows = len(x_mat)
    n_cols = len(x_mat[0])

    # Validate mean
    if mean is not None:
        if not isinstance(mean, list) or len(mean) != n_rows or not all(isinstance(row, list) and len(row) == n_cols for row in mean):
            return "Invalid input: mean must be a 2D list with same shape as x."
        try:
            mean_mat = [[float(val) for val in row] for row in mean]
        except Exception:
            return "Invalid input: mean must contain numeric values."
    else:
        mean_mat = [[0.0 for _ in range(n_cols)] for _ in range(n_rows)]

    # Validate rowcov
    if rowcov is not None:
        if not isinstance(rowcov, list) or len(rowcov) != n_rows or not all(isinstance(row, list) and len(row) == n_rows for row in rowcov):
            return "Invalid input: rowcov must be a square 2D list with shape (n_rows, n_rows)."
        try:
            rowcov_mat = [[float(val) for val in row] for row in rowcov]
        except Exception:
            return "Invalid input: rowcov must contain numeric values."
    else:
        rowcov_mat = [[float(i == j) for j in range(n_rows)] for i in range(n_rows)]

    # Validate colcov
    if colcov is not None:
        if not isinstance(colcov, list) or len(colcov) != n_cols or not all(isinstance(row, list) and len(row) == n_cols for row in colcov):
            return "Invalid input: colcov must be a square 2D list with shape (n_cols, n_cols)."
        try:
            colcov_mat = [[float(val) for val in row] for row in colcov]
        except Exception:
            return "Invalid input: colcov must contain numeric values."
    else:
        colcov_mat = [[float(i == j) for j in range(n_cols)] for i in range(n_cols)]
    # Validate mn_method
    valid_methods = {'pdf', 'logpdf', 'rvs'}
    if mn_method not in valid_methods:
        return f"Invalid input: mn_method must be one of {sorted(valid_methods)}."
    try:
        dist = scipy_matrix_normal(mean=mean_mat, rowcov=rowcov_mat, colcov=colcov_mat)
    except Exception as e:
        return f"scipy.stats.matrix_normal error: {e}"
    if mn_method in ('pdf', 'logpdf'):
        try:
            if mn_method == 'pdf':
                val = dist.pdf(x_mat)
            else:
                val = dist.logpdf(x_mat)
            # Convert numpy types to native float for Excel compatibility
            return [[float(val)]]
        except Exception as e:
            return f"scipy.stats.matrix_normal {mn_method} error: {e}"
    elif mn_method == 'rvs':
        if size is not None:
            try:
                size_int = int(size)
                if size_int < 1:
                    return "Invalid input: size must be >= 1."
            except Exception:
                return "Invalid input: size must be an integer."
        else:
            size_int = 1
        try:
            samples = dist.rvs(size=size_int)
            # If size == 1, samples is a matrix; if size > 1, samples is a 3D array
            if size_int == 1:
                # Return as 2D list
                return [[float(val) for val in row] for row in samples]
            else:
                # Return as 2D list of flattened samples
                return [ [float(val) for row in sample for val in row] for sample in samples ]
        except Exception as e:
            return f"scipy.stats.matrix_normal rvs error: {e}"
    return "Unknown error."

Online Calculator